package de.dnb.gnd.parser;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.InputMismatchException;
import java.util.NoSuchElementException;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;

import de.dnb.basics.Constants;
import de.dnb.basics.applicationComponents.RangeCheckUtils;
import de.dnb.basics.applicationComponents.StreamUtils;
import de.dnb.basics.applicationComponents.StringInputStream;
import de.dnb.basics.filtering.AcceptAll;
import de.dnb.basics.filtering.IPredicate;
import de.dnb.basics.filtering.RejectEmptyStrings;
import de.dnb.basics.filtering.StringContains;
import de.dnb.gnd.exceptions.ExceptionHandler;
import de.dnb.gnd.exceptions.IgnoringHandler;
import de.dnb.gnd.exceptions.WrappingHandler;
import de.dnb.gnd.parser.tag.BibTagDB;
import de.dnb.gnd.parser.tag.GNDTagDB;
import de.dnb.gnd.parser.tag.TagDB;
import de.dnb.gnd.utils.RecordUtils;
import de.dnb.gnd.utils.SubjectUtils;

/**
 * Zum Einlesen einer Reihe von Datensätzen aus einem Strom. Die Datensätzen
 * können in loser Folge mit Befehlen oder anderen Informationen (Command)
 * abwechseln. Die Klasse hat reguläre Ausdrücke, um den Beginn eines
 * Datensatzes oder eines Kommandos zu erkennen.
 * <br><br>
 * Als Handler wird der ignorierende, als Datenbank die GND,
 * als Datensatztrenner "SET: " angesehen.
 * 
 * @author baumann
 *
 */
public class RecordReader {

	private String recordDelimiter;

	private Pattern recordDelimiterPat;

	private Scanner scanner;

	private String nextChunk = null;

	private final RecordParser parser = new RecordParser();

	public final TagDB BIB_TAG_DB = BibTagDB.getDB();

	public final TagDB AUTH_TAG_DB = GNDTagDB.getDB();

	/**
	 *  Für große Datenmengen können die Records schon einmal vorgefiltert 
	 *  werden. Standard ist: keine leeren Strings.
	 */

	private IPredicate<String> streamFilter = new RejectEmptyStrings();

	// Exemplar-Initializer:
	{
		recordDelimiter = "SET: ";
		setDefaultTagDB(GNDTagDB.getDB());

	}

	/**Constructs a new RecordReader that produces values scanned from the 
	 * specified file.
	 * 
	 * @param source	nicht null, nicht leer
	 * @throws FileNotFoundException 
	 */
	public RecordReader(final File source) throws FileNotFoundException {
		RangeCheckUtils.assertReferenceParamNotNull("source", source);
		scanner = new Scanner(source);
		createRecordDelimPat(recordDelimiter);
	}

	/**
	 * Constructs a new RecordReader that produces values scanned from the 
	 * specified file.
	 * @param source		nicht null
	 * @param charsetName	nicht null, nicht leer
	 * @throws FileNotFoundException 
	 * 						wenn File nicht da ...
	 */
	public RecordReader(final File source, final String charsetName)
		throws FileNotFoundException {
		RangeCheckUtils.assertReferenceParamNotNull("source", source);
		RangeCheckUtils.assertStringParamNotNullOrWhitespace(
			"charsetName", charsetName);
		scanner = new Scanner(source, charsetName);
		createRecordDelimPat(recordDelimiter);
	}

	/**
	 * Constructs a new RecordReader that produces values scanned from 
	 * {@link System#in}
	 * 
	 * 
	 */
	public RecordReader() {
		scanner = new Scanner(System.in);
		createRecordDelimPat(recordDelimiter);
	}

	/**
	 * Constructs a new RecordReader that produces values scanned from the 
	 * specified input stream.
	 * 
	 * @param source	nicht null
	 */
	public RecordReader(final InputStream source) {
		RangeCheckUtils.assertReferenceParamNotNull("source", source);
		scanner = new Scanner(source);
		createRecordDelimPat(recordDelimiter);
	}

	/**
	 * Constructs a new RecordReader that produces values scanned from the 
	 * specified input stream.
	 * @param source		nicht null
	 * @param charsetName	nicht null, nicht leer
	 */
	public RecordReader(final InputStream source, final String charsetName) {
		RangeCheckUtils.assertReferenceParamNotNull("source", source);
		RangeCheckUtils.assertStringParamNotNullOrWhitespace(
			"charsetName", charsetName);
		scanner = new Scanner(source, charsetName);
		createRecordDelimPat(recordDelimiter);
	}

	/**
	 * Constructs a new RecordReader that produces values scanned from the 
	 * specified source.
	 * @param source	nicht null
	 */
	public RecordReader(final Readable source) {
		RangeCheckUtils.assertReferenceParamNotNull("source", source);
		scanner = new Scanner(source);
		createRecordDelimPat(recordDelimiter);
	}

	/**
	 * Constructs a new RecordReader that produces values scanned from the 
	 * specified file.
	 * @param sourceFile	nicht null
	 * @throws FileNotFoundException wenn sourcefile nicht existiert
	 */
	public RecordReader(final String sourceFile)
		throws FileNotFoundException {
		RangeCheckUtils.assertReferenceParamNotNull("source", sourceFile);
		File file = new File(sourceFile);
		scanner = new Scanner(file);
		createRecordDelimPat(recordDelimiter);
	}

	/**
	 * Konstruiert einen Reader, der source ausliest.
	 * 
	 * @param source	nicht null
	 * @return			neuen RecordReader
	 */
	public static RecordReader newReader(final String source) {
		RangeCheckUtils.assertReferenceParamNotNull("source", source);
		StringReader reader = new StringReader(source);
		return new RecordReader(reader);
	}

	/**
	 * Changes the recordDelimiter to set.
	 * 
	 * @param aRecordDelimiter may be null.
	 */
	public final void setRecordDelimiter(final String aRecordDelimiter) {
		RangeCheckUtils.assertStringParamNotNullOrEmpty(
			"aRecordDelimiter", aRecordDelimiter);
		createRecordDelimPat(aRecordDelimiter);
		this.recordDelimiter = aRecordDelimiter;
	}

	/**
	 * Erzeugt die Patterns recordDelPat, scannerDelimiterPat 
	 * für Record,  und Chunk .
	 * 
	 * @param aRecordDelimiter	nicht null.
	 */
	private void createRecordDelimPat(final String aRecordDelimiter) {
		RangeCheckUtils.assertReferenceParamNotNull("aRecordDelimiter",
			aRecordDelimiter);
		Pattern.compile(aRecordDelimiter);
		recordDelimiterPat =
			Pattern.compile("(?=(" + aRecordDelimiter + "))");
		scanner.useDelimiter(recordDelimiterPat);
	}

	/**
	 * Ändert den Zeilentrenner. Regulärer Ausdruck als String. 
	 * Wird an den Parser durchgereicht.
	 * 
	 * @param lineDelimiter the line delimiter to set. Not null, not empty.
	 */
	public final void setLineDelimiter(final String lineDelimiter) {
		RangeCheckUtils.assertStringParamNotNullOrEmpty("lineDelimiter",
			lineDelimiter);
		parser.setLineDelimiter(lineDelimiter);
	}

	/**
	 * Ändert die Default-Tag-Datenbank (Wird verwendet, wenn der Satztyp 
	 * nicht sicher erkannt werden kann). Wird an den Parser durchgereicht.
	 * 
	 * @param tagDB nicht null
	 */
	public final void setDefaultTagDB(final TagDB tagDB) {
		RangeCheckUtils.assertReferenceParamNotNull("tagDB", tagDB);
		parser.setDefaultTagDB(tagDB);
	}

	/**
	 * Ändert den Exeption Handler. Wird an den Parser durchgereicht.
	 * @param handler nicht null
	 */
	public final void setHandler(final ExceptionHandler handler) {
		RangeCheckUtils.assertReferenceParamNotNull("handler", handler);
		parser.setHandler(handler);
	}

	/**
	 * Überlese beim Parsen von Pica+-Daten die Felder, die in
	 * MARC21 üblich sind, die aber nicht zum Pica-Format gehören. Wird
	 * an den Parser durchgereicht.
	 * 
	 * @param ignoreMARC	beliebig, wenn true, werden MARC-Felder
	 * 						überlesen. false ist die Standardeinstellung
	 * 						für IBW-Downloads.
	 */
	public void setIgnoreMARC(boolean ignoreMARC) {
		this.parser.setIgnoreMARC(ignoreMARC);
	}

	/**
	 * 
	 * @param stream
	 */
	public void setSource(InputStream stream) {
		scanner = new Scanner(stream);
		scanner.useDelimiter(recordDelimiterPat);
	}

	public void setSource(InputStream inputStream, String charset) {
		scanner = new Scanner(inputStream, charset);
		scanner.useDelimiter(recordDelimiterPat);

	}

	/**
	 * 
	 * @param stream
	 * @throws FileNotFoundException 
	 */
	public void setSource(String filename) throws FileNotFoundException {
		setSource(new File(filename));
	}

	/**
	 * 
	 * @param file
	 * @throws FileNotFoundException
	 */
	public void setSource(File file) throws FileNotFoundException {
		scanner = new Scanner(file);
		scanner.useDelimiter(recordDelimiterPat);
	}

	/**
	 * Empfohlene Einstellungen von GZip-Files:
	 * 
	 *  <br>
	 * <br> RecordDelimiter: \n
	 * <br>	LineDelimiter: RS
	 * <br>	IgnoreMARC: true
	 * <br>	Handler: IgnoringHandler
	 * <br>	DefaultTagDB: BibTagDB.
	 */
	public final void gzipSettings() {
		setRecordDelimiter("\n");
		setLineDelimiter(Constants.RS);
		setIgnoreMARC(true);
		setHandler(new IgnoringHandler()); // bei so großen Files nötig
		setDefaultTagDB(BibTagDB.getDB());
	}

	/**
	 * Empfohlene Standard-Einstellungen:
	 *  <br>
	 * <br> RecordDelimiter: "SET: "
	 * <br>	LineDelimiter: "(\r)?\n"
	 * <br>	IgnoreMARC: false
	 * <br>	Handler: WrappingHandler
	 * <br>	DefaultTagDB: GNDTagDB.
	 */
	public final void defaultSettings() {
		setRecordDelimiter("SET: ");
		setLineDelimiter("(\r)?\n");
		setIgnoreMARC(false);
		setHandler(new WrappingHandler());
		setDefaultTagDB(GNDTagDB.getDB());
	}

	/**
	 * Empfohlene Einstellungen für Datashop-Downloads:
	 *  <br>
	 * <br> RecordDelimiter: GS
	 * <br>	LineDelimiter: RS
	 * <br>	IgnoreMARC: false
	 * <br>	Handler: WrappingHandler
	 * <br>	DefaultTagDB: GNDTagDB.
	 */
	public final void datashopSettings() {
		setRecordDelimiter(Constants.GS);
		setLineDelimiter(Constants.RS);
		setIgnoreMARC(false);
		setHandler(new WrappingHandler());
		setDefaultTagDB(GNDTagDB.getDB());
	}

	/**
	 * 
	 * @param file
	 * @param charset
	 * @throws FileNotFoundException
	 */
	public void setSource(File file, String charset)
		throws FileNotFoundException {
		scanner = new Scanner(file, charset);
		scanner.useDelimiter(recordDelimiterPat);
	}

	/**
	 * Filter, der VOR dem Parsen entscheidet, ob der Datensatz überhaupt
	 * benötigt wird. Für große Datenmengen sinnvoll, da das Parsen
	 * des Datensatzes etwa 95% der Zeit benötigt. Durch sinnvolles
	 * Vorfiltern kann die Bearbeitungszeit auf 10% reduziert werden.
	 *  
	 * Standard ist: keine leeren Strings.
	 * 
	 * @param streamFilter	Prädikat, das Strings akzeptiert, nicht null.
	 */
	public final
		void
		setStreamFilter(final IPredicate<String> streamFilter) {
		RangeCheckUtils.assertReferenceParamNotNull("streamFilter",
			streamFilter);
		this.streamFilter = streamFilter;
	}

	/**
	 * 
	 * Ist ein Datensatz als Nächstes da?
	 * 
	 * @return	true, wenn als Nächstes ein Datensatz anliegt.
	 */
	public final boolean hasNext() {
		if (nextChunk == null) {
			// solange nachladen, bis der nächste Record dem Muster entspricht:
			while (scanner.hasNext()) {
				String next = scanner.next();
				if (streamFilter.accept(next)) {
					nextChunk = next;
					break;
				}
			}
		}
		return nextChunk != null;
	}

	/**
	 * 
	 * @return nächsten Datensatz. Wenn keiner vorhanden, wird eine
	 * 			{@link InputMismatchException} geworfen.
	 */
	public final Record next() {
		if (hasNext()) {
			String s = nextChunk;
			nextChunk = null;
			Record record = parser.parse(s);
			return record;
		} else
			throw new NoSuchElementException("Kein Record mehr da");
	}
	
	@Override
	protected void finalize() throws Throwable {
		if (scanner != null) {
			scanner.close();
		}
		super.finalize();
	}

	public static void main(String[] args) throws IOException {

		RecordReader reader = new RecordReader();
		GZIPInputStream in =
			new GZIPInputStream(new FileInputStream(
				"D:/Normdaten/DNBtitelgesamt.dat.gz"));

		reader.setSource(in);
		reader.gzipSettings();
		// vorab nach 1131 filtern (013D):
		IPredicate<String> titleFilter =
			new StringContains(Constants.RS + "013D " + Constants.US);
		reader.setStreamFilter(titleFilter);

		while (reader.hasNext()) {
			Record record = reader.next();
			System.out.println(RecordUtils.getContentsOfAllSubfields(
				record, "1131", 'a'));
			System.out.println("-----------");
		}

	}

}
